Add session-level auto-approve for AI file read operations#3101
Add session-level auto-approve for AI file read operations#3101mits-pl wants to merge 18 commits intowavetermdev:mainfrom
Conversation
Adds an "Allow reading in this session" button to the AI tool approval UI. When clicked, all subsequent file reads from the same directory are auto-approved for the rest of the session (in-memory only, resets on restart). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds a broad set of AI feature enhancements across frontend and backend. Key additions: session-scoped directory read-approval (frontend UI + RPC + in-memory registry with canonicalization and sensitive-path exclusions); MCP (Model Context Protocol) integration (MCP client, manager, config, HTTP handlers, UI widget, and tools wired into AI requests); persistent session-history and UI banners; multi-step “plan” management and UI panel; project-instructions discovery and a project_instructions tool; new web selector/read/SEO tools; terminal-run tool and execute-from-AI flow; many frontend panels, inputs, and small UX changes; multiple RPC/type updates and routing of MCP/state into AI request flow. Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
|
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
frontend/app/aipanel/aitooluse.tsx (1)
205-212: Apply session approvals before sending final approve action.At Line 211 and Line 291,
handleApprove()runs immediately after firing session-approval RPCs. If those calls are delayed/fail, subsequent reads may still prompt unexpectedly despite the user choosing session allow.💡 Proposed fix
- const handleAllowSession = () => { + const handleAllowSession = async () => { const dirs = extractDirsFromParts(parts); const model = WaveAIModel.getInstance(); - for (const dir of dirs) { - model.sessionReadApprove(dir); - } + await Promise.allSettled(dirs.map((dir) => model.sessionReadApprove(dir))); handleApprove(); }; @@ - const handleAllowSession = () => { + const handleAllowSession = async () => { const dir = extractDirFromToolDesc(toolData.tooldesc); if (dir) { - WaveAIModel.getInstance().sessionReadApprove(dir); + await WaveAIModel.getInstance().sessionReadApprove(dir); } handleApprove(); };Also applies to: 286-292
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/app/aipanel/aitooluse.tsx` around lines 205 - 212, The handler handleAllowSession calls WaveAIModel.getInstance().sessionReadApprove(dir) in a loop then immediately calls handleApprove(), which can race with async RPCs; make handleAllowSession async, collect the promises returned by sessionReadApprove (via extractDirsFromParts(parts)), await Promise.all(promises) and only then call handleApprove(); apply the same change to the other similar handler that performs sessionReadApprove before handleApprove so approvals complete before sending the final approve action.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/app/aipanel/aitooluse.tsx`:
- Around line 80-94: The parent-directory extraction in extractDirFromToolDesc
only checks for '/'; change it to consider both '/' and backslash '\\' by
computing the last separator as the max of filePath.lastIndexOf('/') and
filePath.lastIndexOf('\\') and using that index to derive the parent directory
(preserving the existing special-case behavior for root/leading slash). Update
the logic around lastSlash to use this combined separator index so Windows paths
like "C:\..." return the correct parent directory.
In `@pkg/aiusechat/tools_readdir.go`:
- Around line 168-180: ToolApproval's auto-approval path for read_dir (in the
ToolApproval function that calls parseReadDirInput, wavebase.ExpandHomeDir and
IsSessionReadApproved) lacks the blocked-path guard used by read_text_file, so
session approvals can let protected dirs (e.g. ~/.ssh, ~/.aws or their parent
roots) be enumerated; modify ToolApproval to run the same blocked-path check
used by read_text_file (the verifier that enforces forbidden roots/paths) after
expanding the path and before returning ApprovalAutoApproved, and ensure the
check treats directory roots and parent paths as blocked (not just exact file
matches) so any path under a forbidden root is rejected and returns
ApprovalNeedsApproval.
In `@pkg/aiusechat/tools_readfile.go`:
- Around line 405-417: The current ToolApproval flow auto-approves files in
session-approved parent directories but isBlockedFile() only blocks a few exact
filenames and key-like names, so files under sensitive dirs (e.g., ~/.ssh/*,
~/.aws/*) still get auto-approved; update the isBlockedFile helper to reject
paths that have those directory prefixes (after expanding/home-normalizing the
path) so files like ~/.ssh/config and ~/.aws/config are denied even if their
parent dir is session-approved—locate isBlockedFile and ensure it checks
normalized directory prefixes (e.g., ".ssh", ".ssh/", ".aws", ".aws/") and
returns blocked before ToolApproval/IsSessionReadApproved is consulted, using
the same normalization logic as parseReadTextFileInput/wavebase.ExpandHomeDir.
---
Nitpick comments:
In `@frontend/app/aipanel/aitooluse.tsx`:
- Around line 205-212: The handler handleAllowSession calls
WaveAIModel.getInstance().sessionReadApprove(dir) in a loop then immediately
calls handleApprove(), which can race with async RPCs; make handleAllowSession
async, collect the promises returned by sessionReadApprove (via
extractDirsFromParts(parts)), await Promise.all(promises) and only then call
handleApprove(); apply the same change to the other similar handler that
performs sessionReadApprove before handleApprove so approvals complete before
sending the final approve action.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 1bb03a68-f057-48d5-aaf5-c5eb0e07b77b
📒 Files selected for processing (10)
frontend/app/aipanel/aitooluse.tsxfrontend/app/aipanel/waveai-model.tsxfrontend/app/store/wshclientapi.tsfrontend/types/gotypes.d.tspkg/aiusechat/sessionapproval.gopkg/aiusechat/tools_readdir.gopkg/aiusechat/tools_readfile.gopkg/wshrpc/wshclient/wshclient.gopkg/wshrpc/wshrpctypes.gopkg/wshrpc/wshserver/wshserver.go
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sensitive paths (~/.ssh, ~/.aws, ~/.gnupg, ~/.kube, ~/.password-store, ~/.secrets, keychains, Windows credentials) are now excluded from session auto-approval. Even if a parent directory like ~ is session-approved, reads from these directories still require manual approval. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@pkg/aiusechat/sessionapproval.go`:
- Around line 29-42: The stored approved path must be fully canonicalized
(absolute + symlink-resolved and normalized case on Windows) before inserting
into globalSessionApproval.approvedPaths and candidate paths must be
canonicalized before any prefix checks; update AddSessionReadApproval to call
filepath.Abs then filepath.EvalSymlinks (fall back to the Abs/cleaned value only
if EvalSymlinks returns an error), normalize case on Windows (e.g.,
strings.ToLower), ensure a trailing filepath.Separator, and store that canonical
string in globalSessionApproval.approvedPaths; also update the code that checks
approvals (the code paths invoked by WaveAISessionReadApproveCommand) to apply
the same canonicalization to the requested path before comparing prefixes and
handle canonicalization errors by denying approval or returning a clear error.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 4cb7cfea-2255-44b5-93eb-6d605216c446
📒 Files selected for processing (1)
pkg/aiusechat/sessionapproval.go
Use filepath.EvalSymlinks() on both stored approval roots and candidate paths to prevent symlink-based bypass of session approval boundaries. Also reject sensitive directories at registration time in AddSessionReadApproval. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
♻️ Duplicate comments (1)
pkg/aiusechat/sessionapproval.go (1)
62-99:⚠️ Potential issue | 🟠 MajorNormalize both sides before sensitive/prefix checks (case + canonical roots).
Line 67/Line 83 and Line 115 compare path strings directly, but only the candidate path is canonicalized. Sensitive roots and approval roots are not normalized to the same comparison form (notably case on Windows, and alias/symlink variants of protected roots), so sensitive-path detection and approval matching can be bypassed or behave inconsistently.
Suggested fix
import ( "os" "path/filepath" + "runtime" "strings" "sync" @@ func canonicalizePath(rawPath string) string { @@ return resolved } + +func normalizePathForCompare(rawPath string) string { + p := filepath.Clean(canonicalizePath(rawPath)) + if runtime.GOOS == "windows" { + p = strings.ToLower(p) + } + return p +} @@ func AddSessionReadApproval(dirPath string) { - canonical := canonicalizePath(dirPath) + canonical := normalizePathForCompare(dirPath) @@ func isSensitivePath(expandedPath string) bool { - homeDir := os.Getenv("HOME") - if homeDir == "" { - homeDir = os.Getenv("USERPROFILE") - } - cleanPath := filepath.Clean(expandedPath) + homeDir, _ := os.UserHomeDir() + cleanPath := normalizePathForCompare(expandedPath) @@ for _, dir := range sensitiveDirs { - dirWithSep := dir + string(filepath.Separator) - if cleanPath == dir || strings.HasPrefix(cleanPath, dirWithSep) { + normalizedDir := normalizePathForCompare(dir) + dirWithSep := normalizedDir + string(filepath.Separator) + if cleanPath == normalizedDir || strings.HasPrefix(cleanPath, dirWithSep) { return true } } @@ if localAppData := os.Getenv("LOCALAPPDATA"); localAppData != "" { - credPath := filepath.Join(localAppData, "Microsoft", "Credentials") + credPath := normalizePathForCompare(filepath.Join(localAppData, "Microsoft", "Credentials")) if cleanPath == credPath || strings.HasPrefix(cleanPath, credPath+string(filepath.Separator)) { return true } } if appData := os.Getenv("APPDATA"); appData != "" { - credPath := filepath.Join(appData, "Microsoft", "Credentials") + credPath := normalizePathForCompare(filepath.Join(appData, "Microsoft", "Credentials")) if cleanPath == credPath || strings.HasPrefix(cleanPath, credPath+string(filepath.Separator)) { return true } } @@ func IsSessionReadApproved(filePath string) bool { - canonical := canonicalizePath(filePath) + canonical := normalizePathForCompare(filePath)#!/bin/bash set -euo pipefail echo "Inspect current comparison normalization in session approval logic:" sed -n '27,125p' pkg/aiusechat/sessionapproval.go echo echo "Check whether case normalization is currently present:" rg -n 'ToLower|EqualFold|runtime\.GOOS' pkg/aiusechat/sessionapproval.go echo echo "Check whether sensitive roots are canonicalized before comparison:" rg -n 'sensitiveDirs|isSensitivePath|canonicalizePath\(|normalizePathForCompare' pkg/aiusechat/sessionapproval.goAlso applies to: 107-116
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/aiusechat/sessionapproval.go` around lines 62 - 99, The isSensitivePath logic compares only the candidate path after filepath.Clean, which allows mismatches on case-insensitive OSes and symlink/alias variants; update isSensitivePath (and the related approval-root checks around lines referenced) to canonicalize and normalize both sides before comparing: resolve abs + EvalSymlinks (or fall back to Clean), normalize separators, and on Windows use case-insensitive comparison (strings.EqualFold) or strings.ToLower for both the cleaned expandedPath and each sensitiveDir/approvalRoot; ensure you canonicalize every entry in sensitiveDirs (and the LOCALAPPDATA/APPDATA derived credPath) prior to the equality/HasPrefix checks and use the same dirWithSep normalization for prefix checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@pkg/aiusechat/sessionapproval.go`:
- Around line 62-99: The isSensitivePath logic compares only the candidate path
after filepath.Clean, which allows mismatches on case-insensitive OSes and
symlink/alias variants; update isSensitivePath (and the related approval-root
checks around lines referenced) to canonicalize and normalize both sides before
comparing: resolve abs + EvalSymlinks (or fall back to Clean), normalize
separators, and on Windows use case-insensitive comparison (strings.EqualFold)
or strings.ToLower for both the cleaned expandedPath and each
sensitiveDir/approvalRoot; ensure you canonicalize every entry in sensitiveDirs
(and the LOCALAPPDATA/APPDATA derived credPath) prior to the equality/HasPrefix
checks and use the same dirWithSep normalization for prefix checks.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 102c550f-4cb5-4391-8183-5281b3706968
📒 Files selected for processing (1)
pkg/aiusechat/sessionapproval.go
JSON-RPC 2.0 over stdio client for MCP servers. - Process spawn, handshake, tool discovery - Manager singleton with per-CWD caching - Auto-context (application-info, database-schema) - HTTP API endpoints (/wave/mcp/status, /call, /calllog) - Thread-safe call log with mutex - Read timeout and graceful shutdown
- MCPAccess flag in WaveChatOpts and PostMessageRequest - Auto-context injection (database schema, app info) into all AI backends - MCP tools registered as AI tools (model calls them directly) - MCP system prompt addon with usage guidelines - Current date in system prompt
- MCP Context toggle in AI panel header (alongside Widget Context) - Auto-detect .mcp.json in terminal CWD, show connect banner - Inline MCP connect input from kebab menu - mcpContextAtom, mcpCwdAtom in WaveAIModel
- New 'mcpclient' view in BlockRegistry with CWD input - Tools list with Run button and expandable call log - Registered in launcher sidebar - HTTP endpoints wired in web.go
- web_read_text: extract innerText by CSS selector - web_read_html: extract innerHTML by CSS selector - web_seo_audit: full SEO audit (JSON-LD, OG, meta, headings, alt) - Auto-refresh page before reading - AI reading highlight animation on matched elements - ExecJs option for arbitrary JS execution in webview - InnerText option in WebSelectorOpts
- Save chat history per tab at shutdown - Load and display in AI panel as expandable banner - session_history tool for AI to read previous work - Chat-to-tab mapping registry
- plan_create, plan_status, plan_update tools - Plans persisted to disk, survive restart - Live progress panel in AI panel with expandable steps - Active plan auto-injected into AI context - Dismiss/close plan button
- Context compiler: parse, tag by technology, filter by file extension - Two-step tool: table of contents first, then request specific sections - Reads all instruction files found (WAVE.md + CLAUDE.md combined) - Max 8KB per file, 12KB for requested sections
- Reduce tool description lengths by 60-70% - Consolidate plan/history/instructions into wave_utils multi-action tool - Replace unicode characters with ASCII in AI-visible text - Saves ~2000 tokens per request
- Fix Monaco URI to preserve file extension (.php not .php.orig) - Add language detection from filename for 30+ extensions - Support compound extensions (blade.php)
- Built-in presets: Claude, GPT-5, Gemini, MiniMax, Ollama, OpenRouter - Quick Add Model submenu in kebab menu - Inline API key input with secure storage - Filter presets by available secrets (hide unconfigured) - Ollama connectivity check before activation - Fix: Wave Cloud modes always visible alongside BYOK presets
- Only show execute button for shell language code blocks - Fix imports in aimessage component
Code Review SummaryStatus: 4 Issues Previously Identified | Recommendation: Address remaining issues before merge Overview
Previously Identified Issues (4 total)Status: Partially Addressed
Additional ObservationsNitpick - Potential Race Condition:
Case Normalization (Windows):
Security Model AnalysisThe implementation provides adequate security through layered checks:
The existing critical issues have been addressed. The remaining warnings are suggestions for defense-in-depth improvements. Files Reviewed (50+ files)
Reviewed by minimax-m2.5-20260211 · 2,899,886 tokens |
There was a problem hiding this comment.
Actionable comments posted: 16
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
♻️ Duplicate comments (1)
pkg/aiusechat/tools_readfile.go (1)
405-417:⚠️ Potential issue | 🔴 CriticalStill unresolved: session auto-approve reaches unblocked
~/.sshand~/.awsconfigs.At Line 414, any file under a session-approved parent becomes auto-approved, but
isBlockedFile()still only blocks key-like files and a few exact paths.~/.ssh/config,~/.aws/config, and similar config files still passToolVerifyInput, so approving those directories for the session exposes sensitive material.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/aiusechat/tools_readfile.go` around lines 405 - 417, The session auto-approve logic in ToolApproval can incorrectly approve sensitive config files via an approved parent directory; update the closure in tools_readfile.go so that after expanding the path (ExpandHomeDir(parsed.Filename)) you first check isBlockedFile(expandedPath) (or a new helper that checks for blocked ancestors) and return uctypes.ApprovalNeedsApproval if blocked, and only then call IsSessionReadApproved(expandedPath) to return ApprovalAutoApproved; ensure you reference parseReadTextFileInput, wavebase.ExpandHomeDir, IsSessionReadApproved and isBlockedFile (or implement a blocked-ancestor check) so ToolVerifyInput cannot be bypassed by approving a parent directory.
🟡 Minor comments (15)
frontend/app/monaco/monaco-react.tsx-141-144 (1)
141-144:⚠️ Potential issue | 🟡 MinorAvoid extension parsing from the full path
Line 141-Line 142 parse extensions from the entire path, so a dot in a directory name can be misread as a file extension and produce broken diff URIs.
💡 Proposed fix
- const ext = path.includes(".") ? path.substring(path.lastIndexOf(".")) : ""; - const basePath = path.includes(".") ? encodeURIComponent(path.substring(0, path.lastIndexOf("."))) : encodeURIComponent(path); + const lastSlash = path.lastIndexOf("/"); + const dir = lastSlash >= 0 ? path.slice(0, lastSlash + 1) : ""; + const fileName = lastSlash >= 0 ? path.slice(lastSlash + 1) : path; + const lastDot = fileName.lastIndexOf("."); + const hasExt = lastDot > 0; + const ext = hasExt ? fileName.slice(lastDot) : ""; + const basePath = encodeURIComponent(hasExt ? `${dir}${fileName.slice(0, lastDot)}` : path);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/app/monaco/monaco-react.tsx` around lines 141 - 144, The code currently derives ext and basePath by looking for a dot anywhere in the full path, which mis-detects dots in directory names; update the logic that computes ext and basePath used for origUri and modUri to first isolate the filename (use path.basename on the incoming path) and then compute the extension and name-from-filename (i.e., find the last dot on the filename only). Change the variables ext and basePath (used when building origUri and modUri) so they are based on the filename-derived values (encoded as before) to avoid treating directory dots as file extensions.frontend/app/view/codeeditor/diffviewer.tsx-35-42 (1)
35-42:⚠️ Potential issue | 🟡 MinorMake compound extension detection case-insensitive
Line 41 only matches lowercase
"blade.php", so files likeView.BLADE.PHPwon’t get the intendedhtmllanguage.💡 Proposed fix
function getLanguageFromFileName(fileName: string): string | undefined { - const ext = fileName.split(".").pop()?.toLowerCase(); - // Handle compound extensions like .blade.php - const parts = fileName.split("."); - if (parts.length >= 3) { - const compound = parts.slice(-2).join("."); - if (compound === "blade.php") return "html"; - } + const normalized = fileName.toLowerCase(); + if (normalized.endsWith(".blade.php")) return "html"; + const ext = normalized.split(".").pop(); return ext ? extToLanguage[ext] : undefined; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/app/view/codeeditor/diffviewer.tsx` around lines 35 - 42, The compound-extension check in getLanguageFromFileName only matches "blade.php" in lowercase so files like View.BLADE.PHP are missed; update the comparison to be case-insensitive by normalizing the compound variable (e.g., call toLowerCase() on parts.slice(-2).join(".")) before comparing and ensure any other compound-checks use the same normalization (refer to function getLanguageFromFileName and the compound variable).pkg/aiusechat/projectctx/projectctx.go-137-140 (1)
137-140:⚠️ Potential issue | 🟡 MinorTruncation can break the closing tag.
At Lines 137-140,
result[:maxLen]is taken after</project_instructions>has already been appended. If the slice lands inside that closing tag, the output contains a partial terminator plus another full terminator. Truncate the body first, then append the closing tag once.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/aiusechat/projectctx/projectctx.go` around lines 137 - 140, The truncation currently slices `result` after the closing tag `</project_instructions>` has been appended, which can split that tag; change the logic in the function that builds `result` so you truncate the main body before appending the closing tag: compute/truncate the body to `maxLen` (use the same const maxLen = 8000) and then append `"\n... [truncated]\n</project_instructions>"` (or the full closing tag) once; update any references to `result` accordingly so the final `result` always contains a single, intact `</project_instructions>` terminator.pkg/mcpclient/config.go-17-28 (1)
17-28:⚠️ Potential issue | 🟡 MinorHandle missing
.mcp.jsonpaths inNormalizeMCPDir().The comment says a file path like
/path/to/.mcp.jsonis accepted, but if that file does not existos.Stat()fails and the path is returned unchanged.LoadMCPConfig()then probes/path/to/.mcp.json/.mcp.jsonand returns an error instead of(nil, nil).🛠️ Proposed fix
func NormalizeMCPDir(dir string) string { + if filepath.Base(dir) == MCPConfigFileName { + return filepath.Dir(dir) + } info, err := os.Stat(dir) if err == nil && !info.IsDir() { return filepath.Dir(dir) } return dir }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/mcpclient/config.go` around lines 17 - 28, NormalizeMCPDir currently returns the input unchanged when os.Stat fails, which causes LoadMCPConfig to append MCPConfigFileName twice for inputs like "/path/to/.mcp.json" that don't yet exist; update NormalizeMCPDir to detect if filepath.Base(dir) == MCPConfigFileName and in that case return filepath.Dir(dir) (even if os.Stat returns an error), otherwise keep the existing behavior (if os.Stat succeeds and info.IsDir() is false return filepath.Dir(dir), else return dir). Refer to the functions NormalizeMCPDir and LoadMCPConfig and the constant MCPConfigFileName when making this change.emain/emain-web.ts-52-65 (1)
52-65:⚠️ Potential issue | 🟡 MinorRegister the load waiter before calling
reload().At Lines 53-59,
wc.reload()fires before thedid-finish-loadlistener is attached. On a fast or cached reload that can miss the event, force the timeout path, and then runexecuteJavaScript()while navigation is still in flight.🛠️ Proposed fix
- wc.reload(); - await new Promise<void>((resolve) => { - const onFinish = () => { - wc.removeListener("did-finish-load", onFinish); - resolve(); - }; - wc.on("did-finish-load", onFinish); - // Timeout fallback in case did-finish-load doesn't fire - setTimeout(() => { - wc.removeListener("did-finish-load", onFinish); - resolve(); - }, 10000); - }); + await new Promise<void>((resolve) => { + const onFinish = () => { + clearTimeout(timeoutId); + resolve(); + }; + const timeoutId = setTimeout(() => { + wc.removeListener("did-finish-load", onFinish); + resolve(); + }, 10000); + wc.once("did-finish-load", onFinish); + wc.reload(); + });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@emain/emain-web.ts` around lines 52 - 65, The did-finish-load listener is registered after calling wc.reload(), which can miss the event on fast loads; move the listener registration for "did-finish-load" (the onFinish callback used in the Promise) so it is attached to wc before invoking wc.reload(), keep the 10s timeout fallback and the wc.removeListener cleanup inside both the onFinish and timeout branches, and then await that Promise before calling executeJavaScript() so navigation has completed.frontend/app/aipanel/aipanel.tsx-312-321 (1)
312-321:⚠️ Potential issue | 🟡 MinorDon't turn canceled requests into fake connection failures.
This branch rewrites any
"aborted"error into a network banner, so a normal user cancel/stop path will look like connectivity broke.🧯 Proposed fix
let errorMsg = error.message || "An error occurred"; + const normalized = errorMsg.toLowerCase(); + if (normalized.includes("aborted")) { + return; + } if ( - errorMsg.includes("fetch") || - errorMsg.includes("network") || - errorMsg.includes("ECONNREFUSED") || - errorMsg.includes("Failed to fetch") || - errorMsg.includes("timeout") || - errorMsg.includes("aborted") + normalized.includes("fetch") || + normalized.includes("network") || + normalized.includes("econnrefused") || + normalized.includes("failed to fetch") || + normalized.includes("timeout") ) { errorMsg = "Connection failed — check your internet connection and try again."; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/app/aipanel/aipanel.tsx` around lines 312 - 321, The code is turning cancelled requests into network-failure messages by treating any error message containing "aborted" as a connection failure; update the logic around errorMsg so that cancellations are not mapped to the network banner—remove "aborted" from the generic includes check and instead detect real user/request cancellations explicitly (e.g., check error.name === "AbortError" or error.code === "ERR_CANCELED" or instanceof DOMException with name "AbortError") and skip mapping those to "Connection failed — check your internet connection and try again." Reference the errorMsg variable and the includes-based condition to locate and change the check.pkg/aiusechat/aiplan/handler.go-30-37 (1)
30-37:⚠️ Potential issue | 🟡 MinorUse a non-safe verb for plan deletion.
This handler mutates state but accepts
GETand currently doesn't reject any other method. That makes the delete route easier to trigger accidentally and inconsistent with the method guard inHandlePlanStatus. Please switch this endpoint toPOST/DELETEand reject other methods here, with the caller infrontend/app/aipanel/planprogress.tsxupdated to match.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/aiusechat/aiplan/handler.go` around lines 30 - 37, HandlePlanDelete currently performs state mutation on an HTTP GET and does not guard methods; change it to accept only POST or DELETE (pick one) and return a 405 for other methods. Update the handler function HandlePlanDelete to check r.Method (e.g., allow "POST" or "DELETE" only), call DeletePlan(tabId) as before when allowed, and write a proper JSON error response and 405 status for disallowed methods; also update the caller in frontend/app/aipanel/planprogress.tsx to use the chosen method instead of GET. Ensure Content-Type and success response remain consistent when the delete is performed.frontend/app/aipanel/sessionhistory.tsx-35-48 (1)
35-48:⚠️ Potential issue | 🟡 MinorScope banner UI state to
tabId.
session,expanded, anddismissedsurvive tab switches. A dismissed banner on one tab suppresses fetches for the next tab, and the previous tab's history can stay visible until the new request resolves. Reset or key these states bytabIdbefore fetching the next session.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/app/aipanel/sessionhistory.tsx` around lines 35 - 48, The session, expanded, and dismissed states currently persist across tab switches causing stale UI and suppressed fetches; update the logic in the useEffect (and surrounding component) to reset those states whenever tabId changes before calling fetchSessionHistory: on tabId change call setSession(null), setExpanded(false), setDismissed(false) (or derive these states keyed by tabId) then invoke fetchSessionHistory(tabId) and setSession with the result; ensure the cancellation guard (cancelled) still prevents races and keep tabId in the dependency array so each tab gets a fresh fetch and UI state.frontend/app/aipanel/planprogress.tsx-82-101 (1)
82-101:⚠️ Potential issue | 🟡 MinorIgnore stale fetches when
tabIdchanges.
planandminimizedare carried across tab switches, and the polling effect doesn't guard against in-flight requests from the previous tab. That can briefly render the wrong plan or restore an old plan after a switch. Scope these states totabIdand ignore stale responses in the effect.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/app/aipanel/planprogress.tsx` around lines 82 - 101, PlanProgressPanel currently preserves plan and minimized across tab switches and lets in-flight fetchPlan calls from a previous tab overwrite state; scope these states to tabId and ignore stale responses by resetting plan and minimized when tabId changes and using a request token or AbortController inside the useEffect/refresh flow to validate responses. Specifically, when tabId changes, call setPlan(null) and setMinimized(false), and in refresh (used by useEffect/setInterval) capture a local requestId or attach an AbortController for each fetchPlan call and only call setPlan(result) if the requestId matches the latest or the controller hasn't been aborted; also ensure the interval is cleared and any controller aborted on cleanup so no stale promises update state. Ensure dismissPlan continues to use the current tabId token/abort behavior so it cannot revive state for a different tab.frontend/app/aipanel/apikeyinput.tsx-17-34 (1)
17-34:⚠️ Potential issue | 🟡 MinorGuard
handleSaveagainst re-entry.
Enterstill callshandleSave()while a save is in flight, so repeated key presses can enqueue multipleSetSecretsCommandrequests even though the button is disabled. Addsavingto the early-return guard and dependency list, and optionally disable the input while saving.💡 Minimal fix
const handleSave = useCallback(async () => { - if (!apiKey.trim() || !inputData) return; + if (saving || !apiKey.trim() || !inputData) return; setSaving(true); try { // Save API key as secret await RpcApi.SetSecretsCommand(TabRpcClient, { [inputData.secretName]: apiKey.trim(), @@ - }, [apiKey, inputData, model]); + }, [apiKey, inputData, model, saving]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/app/aipanel/apikeyinput.tsx` around lines 17 - 34, The handleSave function can be re-entered while a save is in flight; update handleSave to return early when saving is true (add "if (saving) return" to the existing guard), include saving in the useCallback dependency array, and optionally disable the input/UI while saving so users can’t trigger another save; locate the handleSave function and related state (apiKey, inputData, saving, model) to apply these changes.pkg/aiusechat/tools_mcp.go-47-66 (1)
47-66:⚠️ Potential issue | 🟡 MinorConsider using the focused block from LayoutState instead of the first terminal.
getTerminalCwdreturns the first terminal block with acmd:cwd, but the Tab model exposes focused-block information viaLayoutState.FocusedNodeIdandLeafOrder. On tabs with multiple terminals, this can point MCP at a different project than the one actively in focus. Consider fetching theLayoutState(usingtab.LayoutState) and resolving the focused node throughLeafOrderto locate the active terminal before falling back to the first terminal.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/aiusechat/tools_mcp.go` around lines 47 - 66, getTerminalCwd currently returns the first terminal block's cwd; change it to prefer the focused terminal from the Tab's layout state: fetch tab.LayoutState, read LayoutState.FocusedNodeId and resolve it through LayoutState.LeafOrder (or equivalent mapping) to get the focused blockId, then try to load that block and return its "cmd:cwd" if it's a terminal; if that lookup fails or the focused node isn't a terminal, fall back to iterating tab.BlockIds as the current implementation does. Update references in getTerminalCwd to use tab.LayoutState, FocusedNodeId, and LeafOrder when resolving the active block before the existing loop.frontend/app/aipanel/mcpdetect.tsx-68-91 (1)
68-91:⚠️ Potential issue | 🟡 MinorReset the dismissed state when the tab changes.
dismissedsurvives prop changes. After a user dismisses the banner in one tab, the early return on Lines 71-74 keeps suppressing MCP detection in later tabs too.🪟 Simple reset
const [detectInfo, setDetectInfo] = useState<MCPDetectInfo | null>(null); const [dismissed, setDismissed] = useState(false); + + useEffect(() => { + setDismissed(false); + }, [tabId]); useEffect(() => { if (mcpEnabled || dismissed) { setDetectInfo(null);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/app/aipanel/mcpdetect.tsx` around lines 68 - 91, The dismissed flag is persisting across tabs and suppressing detection; add a short effect that resets dismissed when the active tab changes by adding useEffect(() => { setDismissed(false); }, [tabId]) (referencing dismissed and setDismissed) so each new tab re-runs MCP detection instead of inheriting the previous dismissal.pkg/mcpclient/handler.go-146-157 (1)
146-157:⚠️ Potential issue | 🟡 MinorSame non-deterministic map iteration for server selection.
Similar to
HandleMCPStatus, whenreq.Serveris empty, the code iterates overconfigsmap non-deterministically to pick a default server.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/mcpclient/handler.go` around lines 146 - 157, The code picks a default server by iterating the configs map non-deterministically (serverName loop) which can vary between runs; change this to select deterministically by collecting the keys from LoadMCPConfig(req.Cwd) into a slice, sort them (e.g., alphabetical), and then set serverName to the first sorted key (or prefer a named default key if present) before proceeding; update the same logic used in HandleMCPStatus to use the sorted-key selection so serverName is deterministic.pkg/aiusechat/aiplan/aiplan.go-102-141 (1)
102-141:⚠️ Potential issue | 🟡 Minor
UpdateStepdoes not validatestatusparameter against allowed values.The function accepts any string as
statuswithout validating it's one of the defined constants (StatusPending,StatusRunning,StatusDone,StatusFailed,StatusSkipped). This could lead to inconsistent plan states.🛡️ Proposed fix to validate status
func UpdateStep(tabId string, stepId int, status string, result string, errMsg string) (*Plan, error) { + // Validate status is a known value + switch status { + case StatusPending, StatusRunning, StatusDone, StatusFailed, StatusSkipped: + // valid + default: + return nil, fmt.Errorf("invalid status: %s", status) + } + plansMu.Lock() defer plansMu.Unlock()🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/aiusechat/aiplan/aiplan.go` around lines 102 - 141, UpdateStep currently accepts any string for the status parameter which can create invalid plan states; add validation at the start of UpdateStep to ensure status is one of the allowed constants (StatusPending, StatusRunning, StatusDone, StatusFailed, StatusSkipped), return a descriptive error if not, and only apply the status update to the matching step after validation; reference the UpdateStep function and the status constants when implementing this check and ensure existing behavior (result/error truncation, DoneAt timestamp, savePlanLocked) remains unchanged.pkg/mcpclient/handler.go-76-116 (1)
76-116:⚠️ Potential issue | 🟡 MinorNon-deterministic server selection when multiple servers are configured.
The loop
for name := range configsiterates over a map, which has non-deterministic order in Go. This means the "first" server selected could vary between calls, leading to inconsistent behavior. If there are multiple servers and only one connects successfully, the user may get different results on repeated calls.Consider sorting the server names for deterministic selection, or documenting this behavior:
♻️ Proposed fix for deterministic server selection
+import "sort" + func HandleMCPStatus(w http.ResponseWriter, r *http.Request) { ... + // Sort server names for deterministic selection + var names []string + for name := range configs { + names = append(names, name) + } + sort.Strings(names) + var resp MCPStatusResponse - for name := range configs { + for _, name := range names { client, err := manager.GetClient(cwd, name)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/mcpclient/handler.go` around lines 76 - 116, Handle the non-deterministic map iteration in HandleMCPStatus by collecting the keys from configs, sorting them, and iterating over the sorted slice instead of using "for name := range configs"; this ensures deterministic server selection when calling manager.GetClient(cwd, name) and building resp (using client.IsAlive(), client.ServerInfo(), client.ListTools(), client.ListResources()) so repeated calls produce consistent results.
🧹 Nitpick comments (15)
frontend/app/aipanel/aimessage.tsx (1)
130-135: Consider handling the case when no terminal block exists.When
findFirstTerminalBlockId()returnsnull, the function silently returns without feedback. Users clicking the execute button may not understand why nothing happened.Additionally,
RpcApi.ControllerInputCommandreturns a Promise that isn't awaited or caught, which could lead to unhandled rejections if the RPC fails.♻️ Suggested improvement with error handling
-function sendCommandToTerminal(cmd: string) { +async function sendCommandToTerminal(cmd: string): Promise<boolean> { const blockId = findFirstTerminalBlockId(); - if (!blockId) return; + if (!blockId) { + console.warn("No terminal block found to execute command"); + return false; + } const b64data = stringToBase64(cmd + "\n"); - RpcApi.ControllerInputCommand(TabRpcClient, { blockid: blockId, inputdata64: b64data }); + try { + await RpcApi.ControllerInputCommand(TabRpcClient, { blockid: blockId, inputdata64: b64data }); + return true; + } catch (e) { + console.error("Failed to send command to terminal", e); + return false; + } }Alternatively, if visual feedback is desired, the callback could trigger a toast/notification when no terminal is available.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/app/aipanel/aimessage.tsx` around lines 130 - 135, The sendCommandToTerminal function silently returns when findFirstTerminalBlockId() yields null and does not handle RPC errors; update sendCommandToTerminal to (1) provide user feedback when no terminal block exists (e.g., call an existing toast/notification helper or emit an event) instead of silently returning, and (2) await RpcApi.ControllerInputCommand(TabRpcClient, {...}) and wrap it in try/catch to handle and log/display any error; reference the function sendCommandToTerminal, findFirstTerminalBlockId, and RpcApi.ControllerInputCommand when making the changes.pkg/aiusechat/chatstore/chatstore.go (1)
52-62: GetAll() should deep-copy chats for consistency with Get().
GetAll()creates a new map but shares the underlying*uctypes.AIChatpointers with the store. This is inconsistent withGet(), which deep-copies the chat (including theNativeMessagesslice) with an explicit comment stating it's to "prevent concurrent access issues." While current usage is read-only and safe, this shallow-copy pattern creates a maintenance risk—future callers might mutate returned chats and cause data races.Align
GetAll()withGet()by deep-copying each chat:Proposed fix
func (cs *ChatStore) GetAll() map[string]*uctypes.AIChat { cs.lock.Lock() defer cs.lock.Unlock() result := make(map[string]*uctypes.AIChat, len(cs.chats)) for k, v := range cs.chats { - result[k] = v + copyChat := &uctypes.AIChat{ + ChatId: v.ChatId, + APIType: v.APIType, + Model: v.Model, + APIVersion: v.APIVersion, + NativeMessages: make([]uctypes.GenAIMessage, len(v.NativeMessages)), + } + copy(copyChat.NativeMessages, v.NativeMessages) + result[k] = copyChat } return result }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/aiusechat/chatstore/chatstore.go` around lines 52 - 62, GetAll currently returns a new map but reuses the store's *uctypes.AIChat pointers, risking races; change GetAll to deep-copy each chat the same way Get() does (including copying the NativeMessages slice and any nested fields) before inserting into the result map. Locate GetAll and either call the existing Get() or extract/reuse the deep-copy logic used by Get() to produce independent *uctypes.AIChat instances while still holding cs.lock during the iteration, then return the map of those copies.frontend/app/aipanel/aimode.tsx (1)
152-157: Optimize to fetch secrets only when dropdown opens.The effect runs on every
isOpenchange, including when closing the dropdown. This causes unnecessary API calls. Consider guarding the fetch to only run whenisOpenistrue.♻️ Suggested fix
// Load available secret names to filter byok presets useEffect(() => { + if (!isOpen) return; RpcApi.GetSecretsNamesCommand(TabRpcClient) .then((names) => setAvailableSecrets(new Set(names || []))) .catch(() => {}); }, [isOpen]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/app/aipanel/aimode.tsx` around lines 152 - 157, The effect currently triggers on every isOpen change (both open and close) causing unnecessary API calls; update the useEffect that calls RpcApi.GetSecretsNamesCommand(TabRpcClient) to first check that isOpen is true and only then call the API, and ensure the result is passed to setAvailableSecrets(new Set(names || [])); keep the existing .catch handler (or add an abort/cleanup if you prefer) but do not call the API when isOpen is false.pkg/aiusechat/anthropic/anthropic-convertmessage.go (1)
147-150: LGTM on tool injection, but debug logging is incomplete.The MCP tools are correctly cleaned and appended to the request, matching the pattern for
TabTools. However, the debug logging below (lines 161-166) only collects tool names fromchatOpts.ToolsandchatOpts.TabTools, omittingchatOpts.MCPTools. This could make debugging MCP tool issues harder.♻️ Suggested fix to include MCP tools in debug logging
for _, tool := range chatOpts.Tools { toolNames = append(toolNames, tool.Name) } for _, tool := range chatOpts.TabTools { toolNames = append(toolNames, tool.Name) } + for _, tool := range chatOpts.MCPTools { + toolNames = append(toolNames, tool.Name) + } if chatOpts.AllowNativeWebSearch { toolNames = append(toolNames, "web_search[server]") }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/aiusechat/anthropic/anthropic-convertmessage.go` around lines 147 - 150, The debug logging that builds the tool-name list currently iterates chatOpts.Tools and chatOpts.TabTools but omits chatOpts.MCPTools, so update the logging code to also iterate chatOpts.MCPTools (using the same Clean() or name extraction logic used for other tool slices) and include those names in the debug output; look for the block that collects tool names for logging and add a loop over chatOpts.MCPTools (or map/append its cleaned names) so MCP tools appear in the debug message alongside TabTools and Tools.pkg/web/web.go (1)
24-27: Import ordering is slightly out of alphabetical order.The imports are functional but
mcpclient(line 27) should come afterfilestorealphabetically. This is a minor style issue.♻️ Suggested import reordering
"github.com/wavetermdev/waveterm/pkg/aiusechat" "github.com/wavetermdev/waveterm/pkg/aiusechat/aiplan" "github.com/wavetermdev/waveterm/pkg/aiusechat/sessionhistory" "github.com/wavetermdev/waveterm/pkg/authkey" - "github.com/wavetermdev/waveterm/pkg/mcpclient" "github.com/wavetermdev/waveterm/pkg/filestore" + "github.com/wavetermdev/waveterm/pkg/mcpclient" "github.com/wavetermdev/waveterm/pkg/panichandler"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/web/web.go` around lines 24 - 27, The import block in pkg/web/web.go is out of alphabetical order: move the "github.com/wavetermdev/waveterm/pkg/mcpclient" import so it appears after "github.com/wavetermdev/waveterm/pkg/filestore" (i.e., sort the imports alphabetically) to match project style; update the import ordering around the existing imports like aiplan, sessionhistory, authkey, filestore and mcpclient so mcpclient comes in the correct position.pkg/aiusechat/sessionhistory/handler.go (1)
25-34: Reuse the package loader instead of duplicating file reads here.This handler re-implements path lookup,
ReadFile, and JSON unmarshal rather than going throughLoadSessionHistory. Keeping two storage paths makes format/path changes easy to drift, and unexpected read/parse failures are currently flattened to{"entries": null}with no shared handling.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/aiusechat/sessionhistory/handler.go` around lines 25 - 34, The handler currently duplicates file reading and JSON unmarshal (using getHistoryFilePath, os.ReadFile, and json.Unmarshal into SessionLog) instead of the shared loader; replace that block with a call to the package loader LoadSessionHistory(tabId) and use its returned SessionLog (or error) so path/format logic is centralized; on error, call the same shared response handling (writeEmptyResponse or the loader's error handling) and remove the manual os.ReadFile/json.Unmarshal code to avoid divergence.pkg/aiusechat/usechat.go (4)
768-775: MCP CWD fallback duplicates CWD lookup logic.This block fetches
getTerminalCwdagain even though it may have been called earlier (line 748). If the consolidation above is applied,tabCwdcan be reused here.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/aiusechat/usechat.go` around lines 768 - 775, The MCP CWD fallback calls getTerminalCwd again instead of reusing the previously computed tabCwd; update the block that sets chatOpts.MCPCwd (guarded by req.MCPAccess and checking req.MCPCwd) to assign chatOpts.MCPCwd = req.MCPCwd when present and otherwise set chatOpts.MCPCwd = tabCwd (the variable computed earlier) rather than calling getTerminalCwd again; reference req.MCPAccess, req.MCPCwd, chatOpts.MCPCwd and tabCwd when making the change.
193-211: Timeout error wrapping may mask the original error.When the context is done, the error is replaced with a generic timeout message. This loses the original error information which could be valuable for debugging (e.g., distinguishing between network errors vs. actual timeouts).
Consider preserving the original error:
if err != nil { if ctx.Err() != nil { - return nil, nil, fmt.Errorf("request timed out - check your internet connection and try again") + return nil, nil, fmt.Errorf("request timed out - check your internet connection and try again: %w", err) } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/aiusechat/usechat.go` around lines 193 - 211, In runAIChatStep, avoid discarding the original error returned by backend.RunChatStep when ctx.Err() != nil; instead wrap or append the original err (from the RunChatStep call) into the timeout message so the caller can inspect the underlying cause (e.g., use fmt.Errorf("request timed out: %w", err) or include err.Error()), and only replace the error when there is truly no underlying err; reference the RunChatStep call and the ctx.Err() check to locate where to change the error handling.
731-761: Multiple redundant tab ID checks and terminal CWD lookups.The code checks
req.TabId != ""four separate times (lines 733, 738, 747, 756) and callsgetTerminalCwdin two places (749, 773). Consider consolidating these checks and caching the CWD lookup result:♻️ Suggested consolidation
+ var tabCwd string if req.TabId != "" { sessionhistory.RegisterChatTab(req.ChatID, req.TabId) - } - - // Notify AI about previous session history availability - if req.TabId != "" { + tabCwd = getTerminalCwd(r.Context(), req.TabId) + + // Notify AI about previous session history availability prevSession := sessionhistory.LoadSessionHistory(req.TabId) if prevSession != "" { chatOpts.SystemPrompt = append(chatOpts.SystemPrompt, "A previous session history exists...") } - } - // Notify AI about project instructions file availability - if req.TabId != "" { - cwd := getTerminalCwd(r.Context(), req.TabId) - if cwd != "" && projectctx.FindInstructionsFile(cwd) != "" { + // Notify AI about project instructions file availability + if tabCwd != "" && projectctx.FindInstructionsFile(tabCwd) != "" { chatOpts.SystemPrompt = append(chatOpts.SystemPrompt, "Project coding instructions file found...") } - } - // Inject active plan status if one exists for this tab - if req.TabId != "" { + // Inject active plan status if one exists for this tab activePlan := aiplan.GetPlan(req.TabId) if activePlan != nil && !aiplan.IsComplete(activePlan) { chatOpts.SystemPrompt = append(chatOpts.SystemPrompt, aiplan.FormatPlanStatus(activePlan)) } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/aiusechat/usechat.go` around lines 731 - 761, Consolidate the repeated req.TabId != "" guards and duplicate getTerminalCwd calls by performing a single if req.TabId != "" block that (1) calls sessionhistory.RegisterChatTab(req.ChatID, req.TabId), (2) loads prevSession via sessionhistory.LoadSessionHistory(req.TabId) and appends the session_history system prompt if non-empty, (3) call getTerminalCwd(r.Context(), req.TabId) once and cache it to use with projectctx.FindInstructionsFile(cwd) to append the project_instructions prompt if found, and (4) fetch activePlan via aiplan.GetPlan(req.TabId) and append aiplan.FormatPlanStatus(activePlan) when activePlan != nil && !aiplan.IsComplete(activePlan); reuse the cached cwd and ensure all appends go to chatOpts.SystemPrompt.
457-465: MCP generation errors are silently logged without notifying the user.When
generateMCPStateAndToolsfails, the error is logged but the chat continues without MCP context. This could lead to confusing behavior where the user expects MCP tools to be available but they aren't. Consider sending a warning to the SSE stream so the frontend can display it.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/aiusechat/usechat.go` around lines 457 - 465, When generateMCPStateAndTools fails in the firstStep branch (the mcpErr != nil case), emit a warning event to the SSE/chat stream so the frontend can display that MCP tools/context are unavailable instead of silently continuing; keep setting chatOpts.MCPState and chatOpts.MCPTools nil (or leave unchanged) and send a short SSE payload (e.g., event type "warning" with a message including mcpErr.Error()) so the UI can surface the problem. Locate the block using generateMCPStateAndTools, mcpErr, chatOpts.MCPState and chatOpts.MCPTools and call the project’s existing SSE emit function (or add one) to send a warning event containing the error string and a clear user-facing message.pkg/mcpclient/manager.go (2)
113-141:GetAutoContextdoes not limit the size of returned context.If
application-infoordatabase-schematools return very large outputs, the context string could become excessively large and consume significant tokens in the AI prompt. Consider adding truncation similar totruncateResultused elsewhere.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/mcpclient/manager.go` around lines 113 - 141, GetAutoContext builds an unbounded context by appending raw outputs from client.CallTool for tools "application-info" and "database-schema"; modify the code paths in GetAutoContext (the loop over clients, the hasToolNamed + client.CallTool blocks) to truncate long results before appending by calling the existing truncateResult helper (or equivalent truncation util) with an appropriate max length (same limit used elsewhere), and append the truncated string (and an indicator like "...(truncated)") instead of the full result so the returned context size is bounded.
221-231: Deleting from map while iterating is safe in Go, but could be clearer.While Go allows deleting from a map during iteration, it's a pattern that can confuse readers. Consider collecting keys first or just iterating without deletion since clearing the map afterward would achieve the same result:
♻️ Alternative implementation
func (m *MCPManager) Shutdown() { m.mu.Lock() defer m.mu.Unlock() for key, client := range m.clients { client.Close() - delete(m.clients, key) + _ = key // suppress unused warning } + m.clients = make(map[string]*MCPClient) log.Println("[mcpclient] all MCP clients shut down") }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/mcpclient/manager.go` around lines 221 - 231, The Shutdown method of MCPManager currently deletes map entries while ranging (MCPManager.Shutdown iterating m.clients and calling client.Close() then delete), which is legal but confusing; change it to lock with m.mu.Lock(), iterate over m.clients and call client.Close() for each entry without deleting, then clear the map in one step (e.g., set m.clients = make(map[string]*MCPClient) or nil) after the loop to make intent clear; keep the defer m.mu.Unlock() and ensure you still call client.Close() on each MCP client.pkg/aiusechat/aiplan/aiplan.go (1)
143-148:DeletePlansilently ignores removal errors.The
os.Removeerror is not checked. While this may be intentional (e.g., file doesn't exist), unexpected errors like permission issues would go unnoticed. Consider at least logging the error.♻️ Proposed fix to log errors
func DeletePlan(tabId string) { plansMu.Lock() defer plansMu.Unlock() - os.Remove(getPlanFilePath(tabId)) + if err := os.Remove(getPlanFilePath(tabId)); err != nil && !os.IsNotExist(err) { + log.Printf("[aiplan] warning: failed to delete plan for tab %s: %v", tabId, err) + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/aiusechat/aiplan/aiplan.go` around lines 143 - 148, The DeletePlan function currently calls os.Remove(getPlanFilePath(tabId)) and ignores its error; change it to capture the error (err := os.Remove(...)) and handle it: if err != nil && !os.IsNotExist(err) then log the error (using the package logger or the standard log package) so permission or other unexpected failures are visible; keep the existing plansMu.Lock()/defer unlock and do not treat "file not found" as an error.pkg/mcpclient/handler.go (2)
208-211:writeJSONignores encoding errors.If
json.NewEncoder(w).Encode(data)fails, the error is silently ignored. While rare, this could mask issues with response serialization.♻️ Proposed fix to log errors
func writeJSON(w http.ResponseWriter, data any) { w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(data) + if err := json.NewEncoder(w).Encode(data); err != nil { + log.Printf("[mcpclient] warning: failed to encode JSON response: %v", err) + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/mcpclient/handler.go` around lines 208 - 211, The writeJSON function currently ignores errors from json.NewEncoder(w).Encode(data); update writeJSON to check the error returned by Encode and handle it (e.g., log the encoding error and respond with an HTTP 500 if the response hasn't been written). Locate writeJSON and change the call to capture err := json.NewEncoder(w).Encode(data), then if err != nil use a logger or log.Printf to record the error (including the data or context) and call http.Error(w, "internal server error", http.StatusInternalServerError) or similar to notify the client.
125-130: Request body is read butdefer r.Body.Close()comes after the read.While technically not a bug (the body will be closed when the handler returns), it's more idiomatic to defer the close immediately after checking the error from
ReadAll. More importantly, ifReadAllfails, the body should still be closed.♻️ Suggested reorder
- body, err := io.ReadAll(r.Body) - if err != nil { - writeJSON(w, MCPCallResponse{Error: "failed to read request body"}) - return - } - defer r.Body.Close() + defer r.Body.Close() + body, err := io.ReadAll(r.Body) + if err != nil { + writeJSON(w, MCPCallResponse{Error: "failed to read request body"}) + return + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/mcpclient/handler.go` around lines 125 - 130, Move the defer r.Body.Close() so the request body is always closed even if io.ReadAll returns an error: after calling io.ReadAll(r.Body) place defer r.Body.Close() before the subsequent if err != nil check (or immediately upon entering the handler), then keep the existing error branch that calls writeJSON(w, MCPCallResponse{Error: ...}) and returns; update only the ordering around io.ReadAll and r.Body.Close() (references: io.ReadAll, r.Body.Close, MCPCallResponse, writeJSON).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: d999e1a2-7591-41b1-b8af-79378888a13d
📒 Files selected for processing (58)
README.mdcmd/server/main-server.goemain/emain-web.tsfrontend/app/aipanel/ai-utils.tsfrontend/app/aipanel/aimessage.tsxfrontend/app/aipanel/aimode.tsxfrontend/app/aipanel/aipanel-contextmenu.tsfrontend/app/aipanel/aipanel.tsxfrontend/app/aipanel/aipanelheader.tsxfrontend/app/aipanel/apikeyinput.tsxfrontend/app/aipanel/mcpdetect.tsxfrontend/app/aipanel/planprogress.tsxfrontend/app/aipanel/sessionhistory.tsxfrontend/app/aipanel/waveai-model.tsxfrontend/app/block/block.tsxfrontend/app/block/blockutil.tsxfrontend/app/element/streamdown.tsxfrontend/app/monaco/monaco-react.tsxfrontend/app/view/codeeditor/diffviewer.tsxfrontend/app/view/mcpclient/mcpclient.tsxfrontend/types/gotypes.d.tspkg/aiusechat/aiplan/aiplan.gopkg/aiusechat/aiplan/handler.gopkg/aiusechat/anthropic/anthropic-convertmessage.gopkg/aiusechat/chatstore/chatstore.gopkg/aiusechat/gemini/gemini-backend.gopkg/aiusechat/openai/openai-convertmessage.gopkg/aiusechat/openaichat/openaichat-convertmessage.gopkg/aiusechat/projectctx/projectctx.gopkg/aiusechat/sessionhistory/handler.gopkg/aiusechat/sessionhistory/registry.gopkg/aiusechat/sessionhistory/saver.gopkg/aiusechat/sessionhistory/sessionhistory.gopkg/aiusechat/tools.gopkg/aiusechat/tools_history.gopkg/aiusechat/tools_mcp.gopkg/aiusechat/tools_plan.gopkg/aiusechat/tools_projectctx.gopkg/aiusechat/tools_readdir.gopkg/aiusechat/tools_readfile.gopkg/aiusechat/tools_screenshot.gopkg/aiusechat/tools_term.gopkg/aiusechat/tools_termrun.gopkg/aiusechat/tools_utils.gopkg/aiusechat/tools_web.gopkg/aiusechat/tools_writefile.gopkg/aiusechat/uctypes/uctypes.gopkg/aiusechat/usechat-prompts.gopkg/aiusechat/usechat.gopkg/mcpclient/config.gopkg/mcpclient/handler.gopkg/mcpclient/manager.gopkg/mcpclient/mcpclient.gopkg/mcpclient/types.gopkg/wconfig/defaultconfig/waveai.jsonpkg/wconfig/defaultconfig/widgets.jsonpkg/web/web.gopkg/wshrpc/wshrpctypes.go
✅ Files skipped from review due to trivial changes (6)
- pkg/wconfig/defaultconfig/widgets.json
- pkg/aiusechat/tools_screenshot.go
- README.md
- pkg/aiusechat/tools_writefile.go
- pkg/aiusechat/tools_term.go
- pkg/mcpclient/types.go
🚧 Files skipped from review as they are similar to previous changes (3)
- pkg/aiusechat/tools_readdir.go
- frontend/types/gotypes.d.ts
- pkg/wshrpc/wshrpctypes.go
| if (opts?.execjs) { | ||
| const customExpr = ` | ||
| (async () => { | ||
| try { | ||
| const result = await (async () => { ${opts.execjs} })(); | ||
| if (Array.isArray(result)) { | ||
| return { value: result.map(String) }; | ||
| } | ||
| return { value: [String(result)] }; | ||
| } catch (error) { | ||
| return { error: error.message }; | ||
| } | ||
| })()`; | ||
| const results = await wc.executeJavaScript(customExpr); | ||
| if (results.error) { | ||
| throw new Error(results.error); | ||
| } | ||
| return results.value; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -n -C3 '\b(execjs|ExecJs)\b' emain pkg frontend
rg -n -C3 '\bwebGetSelector\s*\(' emain pkg frontendRepository: wavetermdev/waveterm
Length of output: 2483
🏁 Script executed:
# Find the definition of seoAuditJS and the webReadContent function
rg -n "seoAuditJS" emain pkg frontend
rg -n "webReadContent" emain pkg frontendRepository: wavetermdev/waveterm
Length of output: 885
🏁 Script executed:
# Check the RPC handler and how data flows from client to webGetSelector
rg -n -A10 "webGetSelector" emain/emain-wsh.tsRepository: wavetermdev/waveterm
Length of output: 1029
🏁 Script executed:
# Find where WebSelectorOpts/WebGetOpts are constructed or passed
rg -n "WebSelectorOpts\|WebGetOpts" emain pkg frontend --type go --type tsRepository: wavetermdev/waveterm
Length of output: 46
🏁 Script executed:
# Find CommandWebSelectorData definition
rg -n "CommandWebSelectorData\|type.*WebSelector" pkg frontend --type ts --type goRepository: wavetermdev/waveterm
Length of output: 46
🏁 Script executed:
# Check where the webselector RPC endpoint is invoked from
rg -n "webselector\|CommandWebSelector" pkg frontend --type ts --type go -iRepository: wavetermdev/waveterm
Length of output: 46
🏁 Script executed:
# Verify seoAuditJS content to understand scope
sed -n '255,300p' pkg/aiusechat/tools_web.goRepository: wavetermdev/waveterm
Length of output: 1531
🏁 Script executed:
# Search for CommandWebSelectorData and WebSelector type definitions
fd -e go -e ts | xargs grep -l "CommandWebSelector\|WebSelectorData" | head -20Repository: wavetermdev/waveterm
Length of output: 258
🏁 Script executed:
# Look at wshrpc protocol definitions
find pkg/wshrpc -type f -name "*.go" | xargs grep -n "WebSelector\|webselector"Repository: wavetermdev/waveterm
Length of output: 764
🏁 Script executed:
# Check if there's a protocol definition file that defines the command structure
fd -e proto -o -e ts -o -e go | xargs grep -l "webselector" 2>/dev/null | head -10Repository: wavetermdev/waveterm
Length of output: 180
🏁 Script executed:
# Check wshclientapi.ts for WebSelectorCommand usage
rg -n -A5 "WebSelectorCommand" frontend/app/store/wshclientapi.tsRepository: wavetermdev/waveterm
Length of output: 411
🏁 Script executed:
# Look at the full CommandWebSelectorData definition
sed -n '482,490p' pkg/wshrpc/wshrpctypes.goRepository: wavetermdev/waveterm
Length of output: 376
🏁 Script executed:
# Search for who calls webselector or WebSelectorCommand from frontend
rg -n "webselector\|WebSelectorCommand" frontend --type tsRepository: wavetermdev/waveterm
Length of output: 46
🏁 Script executed:
# Find all calls to webGetSelector
rg -n "webGetSelector" emain frontend pkg --type ts --type goRepository: wavetermdev/waveterm
Length of output: 384
🏁 Script executed:
# Check if webSelectorInput is parsed from user input
rg -n -B5 -A10 "webSelectorInput" pkg/aiusechat/tools_web.goRepository: wavetermdev/waveterm
Length of output: 1424
🏁 Script executed:
# Search for tool definitions exposed to AI models
rg -n "func.*Tool\|registerTool\|AddTool" pkg/aiusechat/tools_web.go -A3Repository: wavetermdev/waveterm
Length of output: 46
🏁 Script executed:
# Find the tool definitions and how they're registered
rg -n "GetWebSelectorTools\|func.*Web.*Tool" pkg/aiusechat/tools_web.go -B2 -A15Repository: wavetermdev/waveterm
Length of output: 46
🏁 Script executed:
# Check how tools are used/exposed to AI models
rg -n "Tool\|Execute\|Function" pkg/aiusechat/ --type go | grep -i "web\|tool" | head -30Repository: wavetermdev/waveterm
Length of output: 46
🏁 Script executed:
# Look at the beginning of tools_web.go to understand the structure
head -150 pkg/aiusechat/tools_web.goRepository: wavetermdev/waveterm
Length of output: 4305
🏁 Script executed:
# Find the tool function that invokes webReadContent with hardcoded opts
rg -n -B10 "GetWebSelectorToolDefinition\|GetWebReadContentTool" pkg/aiusechat/tools_web.go | head -100Repository: wavetermdev/waveterm
Length of output: 46
🏁 Script executed:
# Check if there's a registry of available tools and how they're used
rg -n "RegisterTool\|AvailableTools\|GetTool" pkg/aiusechat/ --type go | head -20Repository: wavetermdev/waveterm
Length of output: 46
🏁 Script executed:
# Look for the RPC handler registration and validation
rg -n "handle_webselector" emain pkg --type ts --type go -A5Repository: wavetermdev/waveterm
Length of output: 527
🏁 Script executed:
# Search more carefully for tool definitions - look for GetWeb functions
rg -n "^func GetWeb" pkg/aiusechat/tools_web.goRepository: wavetermdev/waveterm
Length of output: 370
🏁 Script executed:
# Check if there's any validation or filtering of opts in the RPC handler
rg -n -A20 "handle_webselector" emain/emain-wsh.tsRepository: wavetermdev/waveterm
Length of output: 1108
🏁 Script executed:
# Search for how tools are exposed - look for tool lists or registrations
rg -n "GetAvailableTools\|tool.*list\|registerTools" pkg/aiusechat --type goRepository: wavetermdev/waveterm
Length of output: 46
🏁 Script executed:
# Check how the AI models use tools - are they exposed via tool definitions or direct RPC?
rg -n "ToolDefinition\|ToolAnyCallback" pkg/aiusechat/tools_web.go | head -30Repository: wavetermdev/waveterm
Length of output: 46
🏁 Script executed:
# Look at the GetWebSEOAuditToolDefinition to see how it calls webReadContent
sed -n '329,400p' pkg/aiusechat/tools_web.goRepository: wavetermdev/waveterm
Length of output: 1227
🏁 Script executed:
# Check if there's any RPC request validation or if all RPC endpoints are freely callable
rg -n "RpcResponseHelper\|validateInput\|checkAccess" emain/emain-wsh.ts | head -20Repository: wavetermdev/waveterm
Length of output: 46
🏁 Script executed:
# Check if RPC endpoints are validated/filtered or if all can be called freely
rg -n "wshRpcCall\|rpc.*webselector" frontend --type ts -iRepository: wavetermdev/waveterm
Length of output: 46
🏁 Script executed:
# Look for the RPC server implementation to see if there's a whitelist of allowed commands
rg -n "RegisterCommand\|CommandHandler\|CommandMap" pkg/wshrpc --type goRepository: wavetermdev/waveterm
Length of output: 46
🏁 Script executed:
# Check if there's any sanitization or validation of opts before it reaches the handler
rg -n -B5 "data.opts" emain/emain-wsh.tsRepository: wavetermdev/waveterm
Length of output: 361
🏁 Script executed:
# Check if there's authentication/authorization for RPC endpoints
rg -n "authenticat\|authoriz\|permission" pkg/wshrpc emain --type go --type ts | head -20Repository: wavetermdev/waveterm
Length of output: 46
🏁 Script executed:
# Look for how the RPC server is set up and if there's endpoint filtering
rg -n "NewWshServer\|RegisterCommand\|handleRequest" pkg/wshrpc --type go | head -20Repository: wavetermdev/waveterm
Length of output: 46
🏁 Script executed:
# Final check: Look at how frontend constructs and sends RPC calls - is there any input validation?
rg -n "wshRpcCall" frontend/app --type ts -A2 | head -30Repository: wavetermdev/waveterm
Length of output: 1609
🏁 Script executed:
# Final verification: check if WebSelectorCommand is ever called from frontend, especially with user input
rg -n "WebSelectorCommand" frontend --type tsRepository: wavetermdev/waveterm
Length of output: 212
🏁 Script executed:
# Check the exact signature and usage of WebSelectorCommand one more time
rg -n -B2 -A8 "WebSelectorCommand.*function\|WebSelectorCommand.*=>.*{" frontend/app/store/wshclientapi.tsRepository: wavetermdev/waveterm
Length of output: 237
🏁 Script executed:
# Search for all calls to WebSelectorCommand in the entire codebase
rg -n "WebSelectorCommand\(" emain pkg frontend --type ts --type goRepository: wavetermdev/waveterm
Length of output: 709
🏁 Script executed:
# Check if there are any other places where the webselector RPC is invoked
rg -n "webselector" frontend --type tsRepository: wavetermdev/waveterm
Length of output: 383
🏁 Script executed:
# Look at the exact line where WebSelectorCommand is called in tools_web.go to see what opts are passed
sed -n '150,180p' pkg/aiusechat/tools_web.goRepository: wavetermdev/waveterm
Length of output: 958
🏁 Script executed:
# Check if there's any validation in the RPC handler for opts parameter
rg -n -A30 "handle_webselector" emain/emain-wsh.tsRepository: wavetermdev/waveterm
Length of output: 1491
🏁 Script executed:
# Search for the RPC server implementation - how are commands routed and validated
fd -e go -path "*/wshrpc/*" -o -path "*/wshutil/*" | xargs grep -l "dispatch\|route\|handle.*command" | head -5Repository: wavetermdev/waveterm
Length of output: 232
🏁 Script executed:
# Check if there's any global RPC validation or filtering mechanism
rg -n "validateCommand\|authorizeCommand\|checkPermission" pkg/wshrpc pkg/wshutil --type goRepository: wavetermdev/waveterm
Length of output: 46
🏁 Script executed:
# Verify: is WebSelectorCommand EVER called from frontend besides in wshclientapi definition?
rg -n "\.WebSelectorCommand\|wshRpcCall.*webselector" frontend --type ts -iRepository: wavetermdev/waveterm
Length of output: 46
🏁 Script executed:
# Look at how RPC messages are routed - check wshutil or wshrpc server code
find pkg/wshrpc pkg/wshutil -name "*.go" | xargs grep -l "Route\|dispatch\|command" | head -3Repository: wavetermdev/waveterm
Length of output: 173
🏁 Script executed:
# Check the RPC server code for how commands are dispatched
rg -n "func.*Route\|func.*Dispatch\|command.*=>" pkg/wshrpc --type go | head -20Repository: wavetermdev/waveterm
Length of output: 46
🏁 Script executed:
# Final verification: Can frontend construct arbitrary RPC calls or are they limited to wshclientapi methods?
rg -n "wshRpcCall\|wshclient" frontend/app/store --type ts | grep -v "wshclientapi\|wshclient.ts" | head -10Repository: wavetermdev/waveterm
Length of output: 46
Add input validation for opts parameter in RPC handler to prevent arbitrary JavaScript execution.
At lines 69–86, this code executes arbitrary JavaScript if opts.execjs is populated. While current AI tool code paths hardcode safe values (only seoAuditJS is used for SEO auditing), the RPC handler at emain-wsh.ts:30 accepts and forwards data.opts directly without validation. Future code changes, frontend modifications, or misuse could introduce user/model-controlled input into execjs.
Add validation in handle_webselector to either:
- Reject/filter unsupported fields in
opts(blockexecjsentirely from RPC) - Only allow safe option combinations (whitelist-based validation)
This protects against accidental exposure and ensures the RPC boundary enforces intent, not just current code patterns.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@emain/emain-web.ts` around lines 69 - 87, The RPC handler handle_webselector
must validate the incoming opts argument to prevent arbitrary execution via
opts.execjs; update handle_webselector to either reject any opts containing the
execjs property or implement a strict whitelist of allowed option keys (e.g.,
only allow seoAuditJS or other explicit safe fields) and return an error when
unknown/blocked keys are present, ensuring that the opts object forwarded to the
emain-web path never contains execjs or other executable payloads; locate
references to opts and execjs in handle_webselector and the emain-web invocation
flow and enforce this validation before forwarding.
| // Local model (Ollama) - check if endpoint is reachable | ||
| try { | ||
| const resp = await fetch("http://localhost:11434/api/tags"); | ||
| if (!resp.ok) throw new Error("not reachable"); | ||
| // Mark as enabled and switch | ||
| await RpcApi.SetSecretsCommand(TabRpcClient, { "byok-local-enabled": "true" }); | ||
| model.setAIMode(presetKey); | ||
| } catch { | ||
| globalStore.set(model.showApiKeyInput, { | ||
| presetKey, | ||
| secretName: "__ollama_error", | ||
| secretLabel: "Ollama not running. Start it with: ollama serve", | ||
| }); | ||
| } |
There was a problem hiding this comment.
Don't funnel Ollama connection failures through the API-key prompt.
The catch path stores a sentinel secretName: "__ollama_error" in showApiKeyInput, but ApiKeyInput treats any payload as a saveable secret. That means a failed Ollama reachability check can still let the user persist a bogus secret and switch into byok@ollama. Use a dedicated error banner/toast or add an explicit non-saveable error variant instead.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/app/aipanel/aipanel-contextmenu.ts` around lines 18 - 31, The
current catch path for the Ollama reachability check incorrectly reuses
model.showApiKeyInput (setting secretName "__ollama_error"), which allows
ApiKeyInput to persist a bogus secret; instead, stop funneling errors through
that saveable flow: replace the catch body so it does not call
globalStore.set(model.showApiKeyInput, ...). Either set a dedicated error state
(e.g., globalStore.set(model.showOllamaError, { presetKey, message: "Ollama not
running. Start it with: ollama serve" })) or call the existing UI
notification/toast API to surface the error; keep RpcApi.SetSecretsCommand and
model.setAIMode unchanged in the success branch and ensure ApiKeyInput only ever
receives genuine secret payloads (do not use secretName "__ollama_error").
| const mcpCwd = globalStore.get(model.mcpCwdAtom); | ||
| const body: any = { | ||
| msg, | ||
| chatid: globalStore.get(model.chatId), | ||
| widgetaccess: globalStore.get(model.widgetAccessAtom), | ||
| mcpaccess: globalStore.get(model.mcpContextAtom), | ||
| aimode: globalStore.get(model.currentAIMode), | ||
| }; | ||
| if (mcpCwd) { | ||
| body.mcpcwd = mcpCwd; | ||
| } |
There was a problem hiding this comment.
Don't send mcpcwd when MCP context is disabled.
mcpcwd is still included whenever it's populated, even if mcpaccess is false. That means turning MCP context off still sends project-path context in the request.
🔒 Proposed fix
- const mcpCwd = globalStore.get(model.mcpCwdAtom);
+ const mcpAccess = globalStore.get(model.mcpContextAtom);
+ const mcpCwd = globalStore.get(model.mcpCwdAtom);
const body: any = {
msg,
chatid: globalStore.get(model.chatId),
widgetaccess: globalStore.get(model.widgetAccessAtom),
- mcpaccess: globalStore.get(model.mcpContextAtom),
+ mcpaccess: mcpAccess,
aimode: globalStore.get(model.currentAIMode),
};
- if (mcpCwd) {
+ if (mcpAccess && mcpCwd) {
body.mcpcwd = mcpCwd;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const mcpCwd = globalStore.get(model.mcpCwdAtom); | |
| const body: any = { | |
| msg, | |
| chatid: globalStore.get(model.chatId), | |
| widgetaccess: globalStore.get(model.widgetAccessAtom), | |
| mcpaccess: globalStore.get(model.mcpContextAtom), | |
| aimode: globalStore.get(model.currentAIMode), | |
| }; | |
| if (mcpCwd) { | |
| body.mcpcwd = mcpCwd; | |
| } | |
| const mcpAccess = globalStore.get(model.mcpContextAtom); | |
| const mcpCwd = globalStore.get(model.mcpCwdAtom); | |
| const body: any = { | |
| msg, | |
| chatid: globalStore.get(model.chatId), | |
| widgetaccess: globalStore.get(model.widgetAccessAtom), | |
| mcpaccess: mcpAccess, | |
| aimode: globalStore.get(model.currentAIMode), | |
| }; | |
| if (mcpAccess && mcpCwd) { | |
| body.mcpcwd = mcpCwd; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/app/aipanel/aipanel.tsx` around lines 290 - 300, The request body is
adding body.mcpcwd whenever mcpCwd exists even if MCP context is disabled;
change the logic in aipanel.tsx so mcpcwd is only added when mcpCwd is present
AND mcp access is enabled: read the mcp access value (model.mcpContextAtom / the
mcpaccess value currently placed into body) into a local variable (or use the
already-read value) and only set body.mcpcwd when both mcpCwd and that mcp
access flag are true, removing the unconditional addition that uses body.mcpcwd.
| <div className="flex items-center text-sm whitespace-nowrap"> | ||
| <span className="text-gray-300 @xs:hidden mr-1 text-[12px]">{shortLabel}</span> | ||
| <span className="text-gray-300 hidden @xs:inline mr-2 text-[12px]">{label}</span> | ||
| <button | ||
| onClick={onToggle} | ||
| className={`relative inline-flex h-6 w-14 items-center rounded-full transition-colors cursor-pointer ${ | ||
| enabled ? "bg-accent-600" : "bg-zinc-600" | ||
| }`} | ||
| title={title} | ||
| > | ||
| <span | ||
| className={`absolute inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${ | ||
| enabled ? "translate-x-8" : "translate-x-1" | ||
| }`} | ||
| /> | ||
| <span | ||
| className={`relative z-10 text-xs text-white transition-all ${ | ||
| enabled ? "ml-2.5 mr-6 text-left" : "ml-6 mr-1 text-right" | ||
| }`} | ||
| > | ||
| {enabled ? "ON" : "OFF"} | ||
| </span> | ||
| </button> |
There was a problem hiding this comment.
Expose a real labeled switch to assistive tech.
The visible label is outside the button, so the accessible name here is effectively just "ON" / "OFF". Add switch semantics and an explicit label, otherwise screen-reader users can't tell the two toggles apart.
♿ Proposed fix
<button
+ type="button"
+ role="switch"
+ aria-checked={enabled}
+ aria-label={label}
onClick={onToggle}
className={`relative inline-flex h-6 w-14 items-center rounded-full transition-colors cursor-pointer ${
enabled ? "bg-accent-600" : "bg-zinc-600"
}`}
title={title}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <div className="flex items-center text-sm whitespace-nowrap"> | |
| <span className="text-gray-300 @xs:hidden mr-1 text-[12px]">{shortLabel}</span> | |
| <span className="text-gray-300 hidden @xs:inline mr-2 text-[12px]">{label}</span> | |
| <button | |
| onClick={onToggle} | |
| className={`relative inline-flex h-6 w-14 items-center rounded-full transition-colors cursor-pointer ${ | |
| enabled ? "bg-accent-600" : "bg-zinc-600" | |
| }`} | |
| title={title} | |
| > | |
| <span | |
| className={`absolute inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${ | |
| enabled ? "translate-x-8" : "translate-x-1" | |
| }`} | |
| /> | |
| <span | |
| className={`relative z-10 text-xs text-white transition-all ${ | |
| enabled ? "ml-2.5 mr-6 text-left" : "ml-6 mr-1 text-right" | |
| }`} | |
| > | |
| {enabled ? "ON" : "OFF"} | |
| </span> | |
| </button> | |
| <div className="flex items-center text-sm whitespace-nowrap"> | |
| <span className="text-gray-300 `@xs`:hidden mr-1 text-[12px]">{shortLabel}</span> | |
| <span className="text-gray-300 hidden `@xs`:inline mr-2 text-[12px]">{label}</span> | |
| <button | |
| type="button" | |
| role="switch" | |
| aria-checked={enabled} | |
| aria-label={label} | |
| onClick={onToggle} | |
| className={`relative inline-flex h-6 w-14 items-center rounded-full transition-colors cursor-pointer ${ | |
| enabled ? "bg-accent-600" : "bg-zinc-600" | |
| }`} | |
| title={title} | |
| > | |
| <span | |
| className={`absolute inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${ | |
| enabled ? "translate-x-8" : "translate-x-1" | |
| }`} | |
| /> | |
| <span | |
| className={`relative z-10 text-xs text-white transition-all ${ | |
| enabled ? "ml-2.5 mr-6 text-left" : "ml-6 mr-1 text-right" | |
| }`} | |
| > | |
| {enabled ? "ON" : "OFF"} | |
| </span> | |
| </button> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/app/aipanel/aipanelheader.tsx` around lines 23 - 45, The toggle
button currently exposes only the "ON"/"OFF" text so screen readers cannot
distinguish controls; update the button (the element with onClick={onToggle}) to
include proper switch semantics by adding role="switch", aria-checked={enabled},
and a clear accessible label via either aria-label or aria-labelledby pointing
to the visible label (create a visually-hidden <span id="..."> or reuse the
existing label spans and reference their id). Also add type="button" to the
button to avoid accidental form submission and keep the existing visual text
("ON"/"OFF") unchanged.
| try { | ||
| const [statusResp, logResp] = await Promise.all([fetchMCPStatus(cwd), fetchCallLog()]); | ||
| setStatus(statusResp); | ||
| setCallLogEntries(logResp || []); | ||
| } catch { | ||
| setStatus({ connected: false, error: "Failed to fetch status" }); | ||
| } |
There was a problem hiding this comment.
Don't couple server health to the call-log endpoint.
Promise.all makes a /wave/mcp/calllog failure fall into the same catch as a real status failure, so Line 281 can mark a healthy MCP server as disconnected. Fetch these independently or use Promise.allSettled.
🩹 One straightforward fix
setLoading(true);
try {
- const [statusResp, logResp] = await Promise.all([fetchMCPStatus(cwd), fetchCallLog()]);
- setStatus(statusResp);
- setCallLogEntries(logResp || []);
+ const [statusResp, logResp] = await Promise.allSettled([fetchMCPStatus(cwd), fetchCallLog()]);
+
+ if (statusResp.status === "fulfilled") {
+ setStatus(statusResp.value);
+ } else {
+ setStatus({ connected: false, error: "Failed to fetch status" });
+ }
+
+ if (logResp.status === "fulfilled") {
+ setCallLogEntries(logResp.value || []);
+ } else {
+ setCallLogEntries([]);
+ }
} catch {
setStatus({ connected: false, error: "Failed to fetch status" });
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| try { | |
| const [statusResp, logResp] = await Promise.all([fetchMCPStatus(cwd), fetchCallLog()]); | |
| setStatus(statusResp); | |
| setCallLogEntries(logResp || []); | |
| } catch { | |
| setStatus({ connected: false, error: "Failed to fetch status" }); | |
| } | |
| try { | |
| const [statusResp, logResp] = await Promise.allSettled([fetchMCPStatus(cwd), fetchCallLog()]); | |
| if (statusResp.status === "fulfilled") { | |
| setStatus(statusResp.value); | |
| } else { | |
| setStatus({ connected: false, error: "Failed to fetch status" }); | |
| } | |
| if (logResp.status === "fulfilled") { | |
| setCallLogEntries(logResp.value || []); | |
| } else { | |
| setCallLogEntries([]); | |
| } | |
| } catch { | |
| setStatus({ connected: false, error: "Failed to fetch status" }); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/app/view/mcpclient/mcpclient.tsx` around lines 276 - 282, The
current Promise.all call couples fetchMCPStatus and fetchCallLog so a call-log
failure will mark the MCP as disconnected; change the logic in the component
where fetchMCPStatus and fetchCallLog are awaited (currently using Promise.all)
to handle them independently—either await fetchMCPStatus alone and
setStatus(statusResp) and then separately await fetchCallLog and
setCallLogEntries(logResp || []), or use Promise.allSettled on
[fetchMCPStatus(), fetchCallLog()] and: if the status promise is fulfilled call
setStatus with its value (or set connected:false on rejection), and if the
callLog promise is fulfilled call setCallLogEntries with the result otherwise
setCallLogEntries([]); keep references to fetchMCPStatus, fetchCallLog,
setStatus, and setCallLogEntries so the changes are easy to locate.
| case "plan_update": | ||
| stepId := int(params["step_id"].(float64)) | ||
| status, _ := params["status"].(string) | ||
| result, _ := params["result"].(string) | ||
| if stepId == 0 { | ||
| return "", fmt.Errorf("step_id required") | ||
| } |
There was a problem hiding this comment.
Validate step_id before converting it.
wave_utils only requires action, so { "action": "plan_update" } is schema-valid today. The direct type assertion on Line 144 will panic instead of returning a normal tool error.
🐛 Safer guard
case "plan_update":
- stepId := int(params["step_id"].(float64))
+ stepIdFloat, ok := params["step_id"].(float64)
+ if !ok || int(stepIdFloat) == 0 {
+ return "", fmt.Errorf("plan_update requires step_id")
+ }
+ stepId := int(stepIdFloat)
status, _ := params["status"].(string)
result, _ := params["result"].(string)
- if stepId == 0 {
- return "", fmt.Errorf("step_id required")
- }
if status == "" {
status = aiplan.StatusDone
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| case "plan_update": | |
| stepId := int(params["step_id"].(float64)) | |
| status, _ := params["status"].(string) | |
| result, _ := params["result"].(string) | |
| if stepId == 0 { | |
| return "", fmt.Errorf("step_id required") | |
| } | |
| case "plan_update": | |
| stepIdFloat, ok := params["step_id"].(float64) | |
| if !ok || int(stepIdFloat) == 0 { | |
| return "", fmt.Errorf("plan_update requires step_id") | |
| } | |
| stepId := int(stepIdFloat) | |
| status, _ := params["status"].(string) | |
| result, _ := params["result"].(string) | |
| if status == "" { | |
| status = aiplan.StatusDone | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@pkg/aiusechat/tools_utils.go` around lines 143 - 149, In the "plan_update"
case, avoid directly asserting params["step_id"].(float64) which can panic;
first check that params["step_id"] exists and is the expected numeric type (use
a type switch or an "ok" style assertion) and return a clear error if it's
missing or not a number, then convert to int and keep the existing zero check
(stepId == 0) and subsequent logic; update the handling around
params["step_id"], stepId, and any error returns in that case block to be
defensive and non-panicking.
| `You can execute commands in the user's terminal using the term_run_command tool. Use it when the user asks you to run a command, check something via CLI, or when running a command would help answer their question (e.g., php artisan, composer, npm, git, ls, etc.). The command requires user approval before execution.`, | ||
|
|
There was a problem hiding this comment.
Gate the term_run_command instructions behind the actual tool list.
GenerateTabStateAndTools only appends term_run_command when the tab has a term widget, so this global prompt gives the model a capability it often doesn't have. The approval sentence also nudges it toward asking first instead of calling the tool and letting runtime approval handle it.
Based on learnings: avoid mentioning approval requirements in tool guidance because it causes redundant pre-approval prompts instead of relying on runtime approval.
Also applies to: 39-41
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@pkg/aiusechat/usechat-prompts.go` around lines 15 - 16, The global prompt
block currently mentions the term_run_command tool and its "requires user
approval" line even when the tab lacks a terminal; update
GenerateTabStateAndTools so that any guidance about term_run_command (including
the approval sentence) is not in the global prompts but is appended only when
GenerateTabStateAndTools detects a tab with a term widget and adds
term_run_command to the tool list; remove the approval sentence from global
guidance entirely and ensure the runtime approval flow is relied upon instead of
nudging pre-approval in the prompt; reference the term_run_command identifier
and the GenerateTabStateAndTools function when making this change.
| // Plans | ||
| `For complex multi-step tasks (auditing multiple pages, processing multiple files, etc.), use the plan_create tool to create an execution plan. Execute one step at a time, call plan_update to record the result, then continue with the next step. If you see an <active_plan> block in your context, continue executing the next pending step immediately.`, | ||
|
|
There was a problem hiding this comment.
Align the planning instructions with wave_utils.
The tool wiring added in this PR advertises a single wave_utils tool, but this prompt tells the model to call standalone plan_create / plan_update tools. As written, the model will emit invalid tool names unless aliases exist elsewhere.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@pkg/aiusechat/usechat-prompts.go` around lines 43 - 45, Update the planning
instruction string in usechat-prompts.go so it references the single advertised
tool "wave_utils" instead of standalone tools; replace the guidance to "call
plan_create / plan_update" with instructions to invoke the "wave_utils" tool
(e.g., call wave_utils with the plan_create or plan_update action) and keep the
existing guidance about executing one step at a time and continuing when an
<active_plan> block is present so the model emits valid tool invocations.
| data, err := json.Marshal(req) | ||
| if err != nil { | ||
| return fmt.Errorf("marshaling request: %w", err) | ||
| } | ||
| data = append(data, '\n') | ||
|
|
||
| if _, err := c.stdin.Write(data); err != nil { | ||
| return fmt.Errorf("writing to stdin: %w", err) | ||
| } | ||
|
|
||
| // Read response line with timeout via channel | ||
| type readResult struct { | ||
| line []byte | ||
| err error | ||
| } | ||
| ch := make(chan readResult, 1) | ||
| go func() { | ||
| line, err := c.stdout.ReadBytes('\n') | ||
| ch <- readResult{line, err} | ||
| }() | ||
|
|
||
| select { | ||
| case res := <-ch: | ||
| if res.err != nil { | ||
| return fmt.Errorf("reading response: %w", res.err) | ||
| } | ||
| var resp JsonRpcResponse | ||
| if err := json.Unmarshal(res.line, &resp); err != nil { | ||
| return fmt.Errorf("parsing response: %w", err) | ||
| } | ||
| if resp.Error != nil { | ||
| return fmt.Errorf("RPC error %d: %s", resp.Error.Code, resp.Error.Message) | ||
| } | ||
| if result != nil && resp.Result != nil { | ||
| if err := json.Unmarshal(resp.Result, result); err != nil { | ||
| return fmt.Errorf("parsing result: %w", err) | ||
| } | ||
| } | ||
| return nil |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Model Context Protocol stdio transport framing: do stdio messages use Content-Length headers for JSON-RPC messages, and can servers send notifications interleaved with responses that must be matched by request id?
💡 Result:
No, stdio messages in the Model Context Protocol do not use Content-Length headers. They use newline-delimited JSON-RPC messages. Yes, servers can send notifications interleaved with responses, and these must be matched to requests by their JSON-RPC request id where applicable (responses match requests by id; notifications have no id).
Citations:
- 1: https://modelcontextprotocol.io/docs/concepts/transports
- 2: https://modelcontextprotocol.io/specification/2025-06-18/basic/transports
- 3: https://modelcontextprotocol.io/specification/draft/basic/transports
- 4: https://modelcontextprotocol.io/specification/2025-11-25/basic/transports
- 5: https://github.com/cyanheads/model-context-protocol-resources/blob/main/guides/mcp-server-development-guide.md
- 6: https://modelcontextprotocol.io/legacy/concepts/transports
- 7: https://modelcontextprotocol.io/specification/latest/basic
Request-response matching and goroutine lifecycle are required for MCP compliance.
At lines 172–188, responses are unmarshaled without matching against the request's id. MCP servers can send notifications interleaved with responses; this code will misroute them. Also, the goroutine spawned at line 169 will remain blocked on c.stdout.ReadBytes('\n') if the timeout fires, leaving a dangling reader and potential resource leak. Implement a per-request dispatcher keyed by id, cancel or reuse the reader lifecycle on timeout, and handle notifications separately from responses.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@pkg/mcpclient/mcpclient.go` around lines 153 - 191, The current per-call
goroutine using c.stdout.ReadBytes and the ad-hoc readResult channel must be
replaced with a request/response dispatcher to avoid misrouting notifications
and leaking readers: implement a single dedicated reader goroutine that
continuously reads lines from c.stdout, unmarshals into JsonRpcResponse, and
dispatches by resp.ID to a synchronized map[id]chan JsonRpcResponse (handle
resp.ID == nil as a server notification path); update the sending function
(which writes to c.stdin and waits) to register a response channel keyed by the
request id, wait with timeout on that channel, and unregister/close the channel
on timeout to avoid leaks; ensure the reader goroutine never blocks on
per-request waits and that notifications are processed via a separate handler
callback or channel.
| ch := make(chan readResult, 1) | ||
| go func() { | ||
| line, err := c.stdout.ReadBytes('\n') | ||
| ch <- readResult{line, err} | ||
| }() | ||
|
|
||
| select { | ||
| case res := <-ch: | ||
| if res.err != nil { | ||
| return fmt.Errorf("reading response: %w", res.err) | ||
| } | ||
| var resp JsonRpcResponse | ||
| if err := json.Unmarshal(res.line, &resp); err != nil { | ||
| return fmt.Errorf("parsing response: %w", err) | ||
| } | ||
| if resp.Error != nil { | ||
| return fmt.Errorf("RPC error %d: %s", resp.Error.Code, resp.Error.Message) | ||
| } | ||
| if result != nil && resp.Result != nil { | ||
| if err := json.Unmarshal(resp.Result, result); err != nil { | ||
| return fmt.Errorf("parsing result: %w", err) | ||
| } | ||
| } | ||
| return nil | ||
| case <-time.After(ReadTimeout): | ||
| return fmt.Errorf("timeout waiting for response to %q after %v", method, ReadTimeout) | ||
| } |
There was a problem hiding this comment.
A timed-out call leaves a live reader on c.stdout.
At Lines 168-194, if time.After fires, the goroutine blocked in ReadBytes keeps running after call() returns. The next RPC can then start a second reader on the same bufio.Reader, which will corrupt response delivery or hang both calls. Close the client on timeout or move all reads into a single dedicated reader goroutine.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@pkg/mcpclient/mcpclient.go` around lines 168 - 194, The current per-call
goroutine that does c.stdout.ReadBytes('\n') in call() can leak when the select
times out, leaving a live reader on c.stdout; fix by either (A) ensuring the
blocking read is canceled on timeout by closing the client/process (call
c.Close() or the underlying io to unblock ReadBytes) before returning from
call() when the time.After fires, or (B) refactor to a single dedicated reader
goroutine that continuously reads lines from c.stdout, parses JsonRpcResponse,
and dispatches results to the waiting call via a response channel keyed by
request (e.g., attach an ID map from request ID to chan JsonRpcResponse), then
update call() to send the request and wait only on that response channel instead
of spawning a ReadBytes goroutine; reference c.stdout, call(), ReadTimeout,
readResult, and JsonRpcResponse to locate and change the logic.
Summary
Changes
pkg/aiusechat/sessionapproval.go— thread-safe in-memory registry for session-approved pathstools_readfile.go/tools_readdir.go—ToolApprovalchecks session allowlist before requiring manual approvalWaveAISessionReadApproveCommandto register approved paths from frontendSecurity
.ssh,.aws, credentials) inisBlockedFile()are enforced viaToolVerifyInputbeforeToolApproval— session approval cannot bypass themTest plan
~/.ssh/id_rsa) are still blocked even with session approval🤖 Generated with Claude Code